﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Net.Sockets;
using System.Net;
using System.Threading;
using log4net;

namespace UDPPunchTest
{
    /// <summary>
    /// The UDPPunchClient registers itself with a well known UDPPunchServer.
    /// The UDPPunchServer tells the UDPPunchClient about other clients and they try to 
    /// communicate with each other directly without involving the PunchServer.
    /// </summary>
    class UDPPunchClient
    {
        private Thread m_client_thread;
        private static readonly ILog log = LogManager.GetLogger(typeof(UDPPunchClient));
        private Socket m_client_socket;
        private IPEndPoint m_server_ep;
        private IPEndPoint m_peer_ep;

        private string m_client_name;
        public string client_name
        {
            private set { }
            get { return m_client_name; }
        }

        private string m_peer_client_name;
        public string peer_client_name
        {
            get { return m_peer_client_name; }
            set { m_peer_client_name = value; }
        }

        public UDPPunchClient(string client_name)
        {
            m_client_name = client_name;
        }

        /// <summary>
        /// Connect to the specified punch server and start listening to incoming data
        /// </summary>
        /// <param name="punch_server_address"></param>
        /// <param name="port"></param>
        public void connect(string punch_server_address,int port)
        {
            m_server_ep = new IPEndPoint(IPAddress.Parse(punch_server_address), port);
            m_client_socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
            m_client_socket.ReceiveTimeout = 5 * 1000;
        }

        public void startClientListener( )
        {
            lock (this)
            {
                if (m_client_thread == null || m_client_thread.ThreadState != ThreadState.Running)
                {
                    if (m_client_thread != null)
                    {
                        m_client_thread.Abort();
                    }
                    m_client_thread = new Thread(listenThread);
                    m_client_thread.Start();
                }
            }
        }

        /// <summary>
        /// The listener thread which waits for incoming packets
        /// </summary>
        private void listenThread( )
        {
            int length = 0;
            byte[] data = new byte[64 * 1024];
            EndPoint remote_ep = new IPEndPoint(IPAddress.Any, 0);
            bool registered = false;
            bool found_peer_address = false;
            bool command_sent = true;
            
            while (true)
            {
                int delay = 250;
                try
                {
                    if (!registered)
                    {
                        registerWithPunchServer(m_client_name);
                    }
                    else if (!found_peer_address)
                    {
                        getPeerClientsAddress(m_peer_client_name);
                    }
                    else
                    {
                        sendStringData(m_client_name + "", m_peer_ep);
                        command_sent = false;
                        Thread.Sleep(10);
                        delay = 60 * 1000;
                    }
                    if (m_client_socket.Available > 0 || command_sent)
                    {
                        length = m_client_socket.ReceiveFrom(data, ref remote_ep);
                        if (length > 0)
                        {
                            PunchMessage pmsg = PunchMessage.parseMessage(data, length);
                            switch (pmsg.m_command)
                            {
                                case "client_registered":
                                    {
                                        registered = true;
                                        log.Info("Registration of client " + m_client_name + " with punch server  " + m_server_ep.ToString() + " successful");
                                        break;
                                    }
                                case "peer_address":
                                    {
                                        string[] f = pmsg.m_command_param.Split(new char[] {':'}, 2);
                                        if (f.Length > 1)
                                        {
                                            found_peer_address = true;
                                            m_peer_ep = new IPEndPoint(IPAddress.Parse(f[0]),Int32.Parse(f[1]));
                                            log.Info("Found address of peer " + m_peer_client_name + ". Address=" + m_peer_ep.ToString());
                                        }
                                        break;
                                    }
                                case PunchMessage.PEER_ADDRESS_NOT_FOUND:
                                    {
                                        log.Info("Cound not find peer address for " + pmsg.m_command_param);
                                        break;
                                    }
                                case "string_data":
                                    {
                                        log.Info("Data=" + pmsg.m_command_param + " received at " + m_client_name);
                                        break;
                                    }
                            }
                        }
                    }
                }
                catch (SocketException e)
                {
                    if (e.ErrorCode != 10060)
                    {
                        log.Error("Got a socket error", e);
                    }
                }
                Thread.Sleep(delay);
            }
        }

        private void getPeerClientsAddress(string peer_name)
        {
            PunchMessage pmsg = new PunchMessage("lookup", peer_name);
            m_client_socket.SendTo(pmsg.serialize(), m_server_ep);
        }

        private void registerWithPunchServer(string name)
        {
            PunchMessage pmsg = new PunchMessage("register", name);
            m_client_socket.SendTo(pmsg.serialize(), m_server_ep);
        }

        private void sendStringData(string data,IPEndPoint addr)
        {
            PunchMessage pmsg = new PunchMessage("string_data", data);
            m_client_socket.SendTo(pmsg.serialize(), addr);
        }
    }
}
